AWS CDKでojosamaのWeb APIを作成する (Lambda + API Gateway)
CloudShell環境なら最初からcdkコマンドも使えるし、npmも使えるので、tscもできるので、環境構築が不要 仕事で使うのであれば、ちゃんとCDK Pipelinesとか構築したほうが良いだろう
構築するものは以下の通り
証明書。APIドメイン用の証明書
がSAMでのインフラ構築とSAMで管理しない部分のインフラ構築の管理の分解に悩んで結局やめた
あとSAMはCloudFormationの拡張みたいな理解で、SAMで作られたリソースをコンソールで手動で修正した後SAM側にインポートする方法も分からなくなった
さらに今回だとモックAPIやらカスタムドメインやらオプションの設定もいろいろ必要で、SAMで設定するのがめんどくさそうになった
色々めんどくさくなったので、SAMは使わずに全部自分でAWS CDKを使ってコード書いた方が分かりやすくて良いだろうと判断した コード全体は以下
code:awscdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import {Construct} from 'constructs';
// ここを必要に応じて修正する
const lambdaBucketName = 'Lambdaのコードを管理するS3バケット名'
const hostedZoneName = 'ホストゾーン名'
const connectionId = 'コネクションID'
const ownerName = 'jiro4989'
function toUpper(name: string): string {
return name0.toUpperCase() + name.slice(1) }
function createLambdaApi(scope: Construct, name: string, props?: cdk.StackProps) {
const idSuffix = toUpper(name)
const logs = new cdk.aws_logs.LogGroup(scope, logGroup${idSuffix}, {
logGroupName: /aws/lambda/${name},
retention: cdk.aws_logs.RetentionDays.ONE_WEEK,
})
const lambdaExecutionRole = new cdk.aws_iam.Role(scope, lambdaExecutionRole${idSuffix}, {
roleName: lambda-execution-role-${name},
assumedBy: new cdk.aws_iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
],
})
const lambdaBucket = cdk.aws_s3.Bucket.fromBucketName(scope, lambdaBucket${idSuffix}, lambdaBucketName)
const lambdaFunction = new cdk.aws_lambda.Function(scope, lambdaFunction${idSuffix}, {
functionName: name,
runtime: cdk.aws_lambda.Runtime.GO_1_X,
code: cdk.aws_lambda.Code.fromBucket(lambdaBucket, ${name}.zip),
handler: 'main',
role: lambdaExecutionRole,
memorySize: 512,
timeout: cdk.Duration.seconds(10),
})
lambdaFunction.node.addDependency(lambdaExecutionRole)
lambdaFunction.node.addDependency(logs)
createApiGateway(scope, name, lambdaFunction)
createDeployPipeline(scope, name, props)
}
function createApiGateway(scope: Construct, name: string, lambdaFunction: cdk.aws_lambda.IFunction) {
const idSuffix = toUpper(name)
const hostedZone = cdk.aws_route53.HostedZone.fromLookup(scope, route53HostedZone${name}, {
domainName: hostedZoneName,
})
const domainName = api.${name}.${hostedZoneName}
const cert = new cdk.aws_certificatemanager.DnsValidatedCertificate(scope, domainName, {
domainName: domainName,
hostedZone: hostedZone,
validation: cdk.aws_certificatemanager.CertificateValidation.fromDns(),
})
const apigw = new cdk.aws_apigateway.RestApi(scope, apiGateway${idSuffix}, {
restApiName: name,
cloudWatchRole: false,
defaultCorsPreflightOptions: {
allowHeaders: [
'Content-Type',
'X-Amz-Date',
'Authorization',
'X-Api-Key',
'Access-Control-Allow-Origin',
],
allowOrigins: cdk.aws_apigateway.Cors.ALL_ORIGINS,
statusCode: 200,
},
})
const integration = new cdk.aws_apigateway.LambdaIntegration(lambdaFunction)
apigw.root.addMethod('POST', integration, {
methodResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Origin": true,
},
},
],
})
const apigwDomain = apigw.addDomainName(apiGatewayDomainName${idSuffix}, {
domainName: domainName,
certificate: cert,
})
new cdk.aws_route53.CnameRecord(scope, route53CnameRecord${idSuffix}, {
zone: hostedZone,
recordName: domainName,
domainName: apigwDomain.domainNameAliasDomainName,
ttl: cdk.Duration.seconds(600),
})
}
function createDeployPipeline(scope: Construct, name: string, props?: cdk.StackProps) {
const idSuffix = toUpper(name)
const region = props?.env?.region || ''
const account = props?.env?.account || ''
const repoMapping = new Map<string, string>()
repoMapping.set('ojosama', 'ojosama-api') // リポジトリ名のゆらぎを吸収
const repoName: string = repoMapping.get(name) || name
// source stage
const output = new cdk.aws_codepipeline.Artifact()
const sourceAction = new cdk.aws_codepipeline_actions.CodeStarConnectionsSourceAction({
actionName: 'GitHub_Source',
owner: ownerName,
repo: repoName,
output: output,
connectionArn: arn:aws:codestar-connections:${region}:${account}:connection/${connectionId},
})
// build stage
const buildRole = new cdk.aws_iam.Role(scope, IamRole${idSuffix}, {
roleName: codebuild-role-${name},
assumedBy: new cdk.aws_iam.ServicePrincipal('codebuild.amazonaws.com'),
})
const s3PolicyStatement = new cdk.aws_iam.PolicyStatement({
resources: [
arn:aws:s3:::${lambdaBucketName},
arn:aws:s3:::${lambdaBucketName}/*,
],
actions: [
's3:PutObject',
],
})
const lambdaPolicyStatement = new cdk.aws_iam.PolicyStatement({
resources: [
arn:aws:lambda:*:*:function:${name},
],
actions: [
'lambda:UpdateFunctionCode',
],
})
const policy = new cdk.aws_iam.Policy(scope, IAMPolicy${idSuffix}, {
policyName: ${name}-codebuild,
statements: [
s3PolicyStatement,
lambdaPolicyStatement,
],
})
buildRole.attachInlinePolicy(policy)
const buildProject = new cdk.aws_codebuild.PipelineProject(scope, CodeBuildProject${idSuffix}, {
projectName: name,
buildSpec: cdk.aws_codebuild.BuildSpec.fromSourceFilename('./buildspec.yml'),
role: buildRole,
environment: {
buildImage: cdk.aws_codebuild.LinuxBuildImage.STANDARD_6_0,
environmentVariables: {
S3_BUCKET: {
type: cdk.aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: lambdaBucketName,
},
},
},
})
const buildAction = new cdk.aws_codepipeline_actions.CodeBuildAction({
actionName: 'build',
project: buildProject,
input: output,
runOrder: 2,
})
new cdk.aws_codepipeline.Pipeline(scope, CodePipeline${idSuffix}, {
pipelineName: name,
crossAccountKeys: false,
stages: [
{
stageName: 'Source',
},
{
stageName: 'Build',
},
],
})
}
export class AWSCDKStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
createLambdaApi(this, 'アプリケーション名', props)
// example resource
// const queue = new sqs.Queue(this, 'AwscdkQueue', {
// visibilityTimeout: cdk.Duration.seconds(300)
// });
}
}
createLambdaApiという関数を呼び出すだけで、諸々のリソースがまとめて作成される
事前準備
CodePipelineの画面でGitHubとの接続を作成する必要がある。connectionArnが必要なため
AWS Lambda用のS3バケットと、中身がすかすかでも良いのでbuildspec.ymlを含んだZIPファイルが必要 構成としてはこう
且つ、流石にインフラのコードを全部パブリックな場所に公開するのはちょっと心配だったのでプライベートなリポジトリにしている
アプリケーションのコードはパブリックリポジトリで管理
Lambdaのupdate-function-codeもアプリケーション側で行う
buildspec.ymlだけを管理して、インフラへのupdate-function-code実行はCodeBuild上で行う
これにより、GitHubリポジトリ側では秘匿情報を管理しなくてよい
デメリットとして、アプリケーション側ではLambdaのランタイムやメモリサイズの制御ができない
アプリケーションのコードくらいしかいじれない
Codebuildに割り当てているIAMロールも最低限の権限しか与えていない
一連のインフラ構築とアプリケーション更新のフローを図示すると以下のようになる
https://gyazo.com/2fbfffadd6b062925005ccba1556877f
code:plantuml
@startuml
actor 開発者 as dev
cloud AWS {
package CDK実行環境 {
}
package デプロイ機構 {
}
package LambdaAPI {
}
}
node GitHubリポジトリ as gh
dev -do-> shell : 1 cdk deploy
shell -do-> cfn : 2 cdk deploy
cfn -do-> lambda : 3 インフラ構築
cfn -do-> api : 3 インフラ構築
cfn -le-> deploy : 3 インフラ構築
cfn -le-> build : 3 インフラ構築
cfn -do-> cname : 3 インフラ構築
dev -do-> gh : 4 git push
deploy -up-> gh : 5 更新チェック
deploy -do-> build : 6 起動
build -do-> lambda : 7 update-function-code
lambda - api
api - cname
@enduml
アプリケーション側
buildspec.yml
code:buildspec.yml
---
version: 0.2
env:
variables:
HANDLER: main
SOURCE_NAME: ojosama.zip
phases:
install:
runtime-versions:
golang: 1.18
commands:
- go mod download
pre_build:
commands:
- go vet .
- go test -cover ./...
build:
commands:
- go build -o "${HANDLER}"
- zip "${SOURCE_NAME}" "${HANDLER}"
post_build:
commands:
- aws s3 cp "./${SOURCE_NAME}" "s3://${S3_BUCKET}/"
- aws lambda update-function-code --function-name ojosama --s3-bucket "${S3_BUCKET}" --s3-key "${SOURCE_NAME}"
ハンドラー関数
一通りリソースを作成して、curlでは成功するのにフロントからアクセスするとCORSで弾かれるのに悩んでたが、アプリケーション側で明示的にAccess-Control-Allow-Originヘッダを設定して上げれば良かった code:go
return events.APIGatewayProxyResponse{
StatusCode: 200,
Body: string(b),
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*", // ← これが必要だった
},
}, nil
}
これで関数呼び出しを一個追加するだけで、全く同じ構成のサーバレスAPIが完成する
この記事書いててスロットリングの設定忘れてた事に気づいたので、後で追加する
以上
参考資料